package tcln.sors.client.core.mvc;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Observable;

import tcln.commons.collections.CollectionUtils;
import tcln.commons.exception.BaseRuntimeException;
import tcln.commons.lang.ObjectUtils;

/**
 * Default implementation of a {@link ModelData} which relies on a {@link LinkedHashMap} to provide access to all model
 * properties.
 * 
 * @param <M>
 *            Parameter type for DSL in this model
 * @author Truong Chau Lien Ngoc (lienngoc@gmail.com)
 */
public class DefaultModelData<M extends ModelData<?>> extends Observable implements ModelData<M> {

    /**
     * A flag indicating if 'property change' should be monitored or not. If this property is reset, setting a new value
     * to a property in this model neither marks this property 'dirty' nor notifies observable objects.
     */
    protected boolean propertyChangedMonitored = true;

    /**
     * All properties' values, identified by properties' names.
     */
    protected Map<String, Object> propertyMap = CollectionUtils.newLinkedHashMap();

    /**
     * All 'dirty' attributes for all properties, identified by properties' names.
     */
    protected Map<String, Boolean> dirtyAttributeMap = CollectionUtils.newHashMap();

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public M getThis() {
        return (M) this;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public <V> V get(String property, Class<V> propertyClass) {
        Object v = propertyMap.get(property);
        if (v != null) {
            if (propertyClass.isAssignableFrom(v.getClass())) {
                return (V) v;
            } else {
                throw new BaseRuntimeException("Property {0} cannot be converted to {1}", property, propertyClass);
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <V> M set(String property, V value) {
        Object oldValue = propertyMap.get(property);
        propertyMap.put(property, value);

        // if this property had been already set and then is changed, this property should be marked 'dirty'
        if (oldValue != null && !ObjectUtils.equals(oldValue, value) && propertyChangedMonitored) {
            dirtyAttributeMap.put(property, true);
            notifyObservers(new ModelDataNotification().forProperty(property).fromValue(oldValue).toValue(value));
        }

        return getThis();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Object> getPropertyValues() {
        return new ArrayList<Object>(propertyMap.values());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public M clear() {
        propertyMap.clear();
        dirtyAttributeMap.clear();
        return getThis();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isDirty(String property) {
        return dirtyAttributeMap.containsKey(property);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isDirty() {
        return !dirtyAttributeMap.isEmpty();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, Object> toMap(boolean full) {
        Map<String, Object> resultMap = null;
        if (full) {
            resultMap = new LinkedHashMap<String, Object>(propertyMap);
        } else {
            resultMap = CollectionUtils.newLinkedHashMap();
            for (Entry<String, Object> propEntry : propertyMap.entrySet()) {
                if (isDirty(propEntry.getKey())) {
                    resultMap.put(propEntry.getKey(), propEntry.getValue());
                }
            }
        }
        
        return resultMap;
    }
}
